﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Management;
using Microsoft.Win32;
using WinReg = Microsoft.Win32.Registry;

namespace System.Peppers.Wmi
{
    /// <summary>
    /// Combines Win32_WindowsProductActivation with some registry manipulation so you can do whatever you want to do with Windows keys
    /// *NOTE: Setting your key is effective only on Volume License media. If you try delete or reset your key on OEM media or on retail media, you will not change the product key.  This means if you have a dell you cannot set or change your key, but you can retrieve it.
    /// *NOTE: not tested on Vista yet, might have to rework for vista
    /// </summary>
    public sealed class WindowsProductActivation : CimSettingID
    {
        #region Static
        private static class Registry
        {
            public const string ProductIDKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion",
                ProductIDValue = "DigitalProductId",
                ActivateKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\WPAEvents",
                ActivateValue = "OOBETimer";
        }

        private static class WmiProperties
        {
            public const string ActivationRequired = "ActivationRequired",
                IsNotificationOn = "IsNotificationOn",
                ProductID = "ProductID",
                RemainingEvaluationPeriod = "RemainingEvaluationPeriod",
                RemainingGracePeriod = "RemainingGracePeriod",
                ServerName = "ServerName";
        }

        private static class WmiMethods
        {
            public const string ActivateOffline = "ActivateOffline",
                ActivateOnline = "ActivateOnline",
                GetInstallationID = "GetInstallationID",
                SetNotification = "SetNotification",
                SetProductKey = "SetProductKey";
        }

        private static class WmiParameters
        {
            public const string ConfirmationID = "ConfirmationID",
                InstallationID = "InstallationID",
                Enable = "Enable",
                ProductKey = "ProductKey";
        }

        private static class Util
        {
            private static readonly char[] DIGITS = new char[]
              {
                'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'P', 'Q', 'R', 
                'T', 'V', 'W', 'X', 'Y', '2', '3', '4', '6', '7', '8', '9',
              };
            // Offset of first byte of encoded product key in 
            //  'DigitalProductIdxxx" REG_BINARY value. Offset = 34H.
            private const int KEY_START_INDEX = 52;
            // Offset of last byte of encoded product key in 
            //  'DigitalProductIdxxx" REG_BINARY value. Offset = 43H.
            private const int KEY_END_INDEX = KEY_START_INDEX + 15;
            // Possible alpha-numeric characters in product key.           
            // Length of decoded product key
            private const int DECODE_LENGTH = 29;
            // Length of decoded product key in byte-form.
            // Each byte represents 2 chars.
            private const int DECODE_STRING_LENGTH = 15;

            public static string DecodeProductKey(byte[] digitalProductId)
            {
                if (digitalProductId == null)
                {
                    return null;
                }
                // Array of containing the decoded product key.
                char[] decodedChars = new char[DECODE_LENGTH];
                // Extract byte 52 to 67 inclusive.
                List<byte> hexPid = new List<byte>();
                for (int i = KEY_START_INDEX; i <= KEY_END_INDEX; i++)
                {
                    hexPid.Add(digitalProductId[i]);
                }
                for (int i = DECODE_LENGTH - 1; i >= 0; i--)
                {
                    // Every sixth char is a separator.
                    if ((i + 1) % 6 == 0)
                    {
                        decodedChars[i] = '-';
                    }
                    else
                    {
                        // Do the actual decoding.
                        int digitMapIndex = 0;
                        for (int j = DECODE_STRING_LENGTH - 1; j >= 0; j--)
                        {
                            int byteValue = (digitMapIndex << 8) | (byte)hexPid[j];
                            hexPid[j] = (byte)(byteValue / 24);
                            digitMapIndex = byteValue % 24;
                            decodedChars[i] = DIGITS[digitMapIndex];
                        }
                    }
                }
                return new string(decodedChars);
            }
        }

        /// <summary>
        /// Enumerates windows activation objects on the system
        /// </summary>
        /// <returns>List of windows activation objects</returns>
        public static IEnumerable<WindowsProductActivation> GetActivation()
        {
            foreach (ManagementObject item in GetInstances(WmiClasses.Win32_WindowsProductActivation))
            {
                yield return new WindowsProductActivation(item);
            }
        }

        /// <summary>
        /// Enumerates windows activation objects on a remote system
        /// </summary>
        /// <param name="remoteParams">Connection options for remote system</param>
        /// <returns>List of windows activation objects</returns>
        public static IEnumerable<WindowsProductActivation> GetActivation(WmiRemoteParameters remoteParams)
        {
            foreach (ManagementObject item in GetInstances(WmiClasses.Win32_WindowsProductActivation, remoteParams))
            {
                yield return new WindowsProductActivation(item);
            }
        }
        #endregion

        internal WindowsProductActivation(ManagementObject wmiObject)
            : base(wmiObject)
        { }

        /// <summary>
        /// If true, system activation is pending for the system. The system must be activated within the number of days indicated by the RemainingGracePeriod property. If false, activation is not required during a specific time period.
        /// </summary>
        public bool ActivationRequired
        {
            get { return Convert.ToUInt32(this[WmiProperties.ActivationRequired] ?? 0) != 0; }
        }

        /// <summary>
        /// If equal to true, and product activation is required, notification reminders (message balloons) are enabled and the activation icon appears in the notification tray. If false, notification reminders and the activation icon are disabled.
        /// NOTE: available only on XP SP1 and above
        /// </summary>
        public bool IsNotificationOn
        {
            get { return Convert.ToUInt32(this[WmiProperties.ActivationRequired] ?? 0) != 0; }
        }

        /// <summary>
        /// String of 20 characters separated by hyphens in the format, xxxxx-xxx-xxxxxxx-xxxxx. This is the same product ID that is displayed under the General tab of the System Properties dialog in Control Panel.
        /// </summary>
        public string ProductID
        {
            get { return Convert.ToString(this[WmiProperties.ProductID]); }
        }

        /// <summary>
        /// If this instance represents beta or evaluation media, this represents the number of days remaining before expiration of the media. Otherwise, this property is set to the largest possible unsigned value.
        /// </summary>
        public uint RemainingEvaluationPeriod
        {
            get { return Convert.ToUInt32(this[WmiProperties.RemainingEvaluationPeriod] ?? 0); }
        }

        /// <summary>
        /// Number of days remaining before activation of the system is required, if the ActivationRequired property is equal to 1.
        /// </summary>
        public uint RemainingGracePeriod
        {
            get { return Convert.ToUInt32(this[WmiProperties.RemainingGracePeriod] ?? 0); }
        }

        /// <summary>
        /// System whose WPA properties and methods are to be accessed. This property is a string that specifies the name of the computer or its IP address.
        /// </summary>
        public string ServerName
        {
            get { return Convert.ToString(this[WmiProperties.ServerName]); }
        }

        /// <summary>
        /// The ActivateOffline WMI class method permits offline system activation using the confirmation ID provided by the Microsoft Clearinghouse license server. You must call the GetInstallationID method before calling this method.
        /// </summary>
        /// <param name="confirmationID">Confirmation ID obtained from the Microsoft Clearinghouse license server. This value is generated from the installation ID.</param>
        /// <returns>Result of method</returns>
        public ManagementStatus ActivateOffline(string confirmationID)
        {
            return ExecuteStatusMethod(WmiMethods.ActivateOffline, delegate(ManagementBaseObject inParam)
            {
                inParam[WmiParameters.ConfirmationID] = confirmationID;
            });
        }

        /// <summary>
        /// The ActivateOnline WMI class method exchanges license-related data with the Microsoft Clearinghouse license server. If the exchange succeeds, this method completes system activation. This method requires that the target computer communicate through the Internet using the HTTPS protocol.
        /// </summary>
        /// <returns>Result of method</returns>
        public ManagementStatus ActivateOnline()
        {
            return ExecuteStatusMethod(WmiMethods.ActivateOnline, null);
        }

        /// <summary>
        /// The GetInstallationID WMI class method retrieves the installation ID.
        /// </summary>
        /// <param name="installationID">The installation ID is a string generated from the product ID (PID) and the computer's hardware ID. This string is identical to the installation ID that is displayed on the telephone activation page and is 50 digits in length. The customer must supply the installation ID to the Microsoft Clearinghouse license server to obtain a confirmation ID. The confirmation ID is required to call the ActivateOffline method.</param>
        /// <returns>Result of method</returns>
        public ManagementStatus GetInstallationID(out string installationID)
        {
            using (ManagementBaseObject outParam = ExecuteMethod(WmiMethods.GetInstallationID, null))
            {
                installationID = Convert.ToString(outParam[WmiParameters.InstallationID]);
                return (ManagementStatus)Convert.ToUInt32(outParam[WmiClasses.ReturnValue]);
            }
        }

        /// <summary>
        /// The SetNotification WMI class method enables or disables the display of notification reminders (message balloons) and the activation icon in the notification tray.
        /// Calling this method has no effect on the length of the activation grace period nor does it affect whether system activation is required. If activation is not required, or if activation is not pending, calling this method also has no effect. 
        /// </summary>
        /// <param name="enable">If equal to true and activation is required, notification reminders are enabled and the activation icon appears in the notification tray. If false, notification reminders and the activation icon are disabled. Note that notifications are enabled by default. For more information, see the following Remarks section.</param>
        /// <returns>Result of method</returns>
        public ManagementStatus SetNotification(bool enable)
        {
            return ExecuteStatusMethod(WmiMethods.SetNotification, delegate(ManagementBaseObject inParam)
            {
                inParam[WmiParameters.Enable] = enable ? 1u : 0u;
            });
        }

        /// <summary>
        /// The SetProductKey WMI class method updates the system product key for a computer.
        /// This method is only valid if the ActivationRequired property is equal to 1.
        /// </summary>
        /// <param name="productKey">Unique product key that is licensed for use on the computer. Product keys are alphanumeric strings of 25 characters formatted as follows: xxxxx-xxxxx-xxxxx-xxxxx-xxxxx. The product key must be valid for the media type. Media types include retail, volume-licensing, and OEM.</param>
        /// <returns></returns>
        public ManagementStatus SetProductKey(string productKey)
        {
            return ExecuteStatusMethod(WmiMethods.SetProductKey, delegate(ManagementBaseObject inParam)
            {
                inParam[WmiParameters.ProductKey] = productKey;
            });
        }

        /// <summary>
        /// Retrieves an decodes your true product key from the registry
        /// </summary>
        /// <returns>Your product key in string form</returns>
        public string GetProductKey()
        {
            using (RegistryKey key = WinReg.LocalMachine.OpenSubKey(Registry.ProductIDKey, true))
            {
                byte[] productKey = key.GetValue(Registry.ProductIDValue, null) as byte[];
                return Util.DecodeProductKey(productKey);
            }
        }

        /// <summary>
        /// Deactivates windows by deleting a registy value
        /// </summary>
        public void Deactivate()
        {
            using (RegistryKey key = WinReg.LocalMachine.OpenSubKey(Registry.ActivateKey, true))
            {
                key.DeleteValue(Registry.ActivateValue, false);
            }
            using (RegistryKey key = WinReg.LocalMachine.OpenSubKey(Registry.ProductIDKey, true))
            {
                key.SetValue(Registry.ProductIDValue, new byte[0]);
            }
        }
    }
}
